/*
 * $Id: JXRootPane.java 4147 2012-02-01 17:13:24Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.LayoutManager;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JRootPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;

import org.jdesktop.beans.JavaBean;

/**
 * Extends the JRootPane by supporting specific placements for a toolbar and a
 * status bar. If a status bar exists, then toolbars, menus will be registered
 * with the status bar.
 * 
 * @see JXStatusBar
 * @author Mark Davidson
 */
@JavaBean
public class JXRootPane extends JRootPane {
	/**
	 * An extended {@code RootLayout} offering support for managing the status
	 * bar.
	 * 
	 * @author Karl George Schaefer
	 * @author Jeanette Winzenberg
	 */
	protected class XRootLayout extends RootLayout {

		LayoutManager2 delegate;

		/**
		 * The layout manager backing this manager. The delegate is used to
		 * calculate the size when the UI handles the window decorations.
		 * 
		 * @param delegate
		 *            the backing manager
		 */
		public void setLayoutManager(LayoutManager2 delegate) {
			this.delegate = delegate;
		}

		private Dimension delegatePreferredLayoutSize(Container parent) {
			if (delegate == null)
				return super.preferredLayoutSize(parent);
			return delegate.preferredLayoutSize(parent);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Dimension preferredLayoutSize(Container parent) {
			Dimension pref = delegatePreferredLayoutSize(parent);
			if (statusBar != null && statusBar.isVisible()) {
				Dimension statusPref = statusBar.getPreferredSize();
				pref.width = Math.max(pref.width, statusPref.width);
				pref.height += statusPref.height;
			}
			return pref;
		}

		private Dimension delegateMinimumLayoutSize(Container parent) {
			if (delegate == null)
				return super.minimumLayoutSize(parent);
			return delegate.minimumLayoutSize(parent);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Dimension minimumLayoutSize(Container parent) {
			Dimension pref = delegateMinimumLayoutSize(parent);
			if (statusBar != null && statusBar.isVisible()) {
				Dimension statusPref = statusBar.getMinimumSize();
				pref.width = Math.max(pref.width, statusPref.width);
				pref.height += statusPref.height;
			}
			return pref;

		}

		private Dimension delegateMaximumLayoutSize(Container parent) {
			if (delegate == null)

				return super.maximumLayoutSize(parent);
			return delegate.maximumLayoutSize(parent);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Dimension maximumLayoutSize(Container target) {
			Dimension pref = delegateMaximumLayoutSize(target);
			if (statusBar != null && statusBar.isVisible()) {
				Dimension statusPref = statusBar.getMaximumSize();
				pref.width = Math.max(pref.width, statusPref.width);
				// PENDING JW: overflow?
				pref.height += statusPref.height;
			}
			return pref;
		}

		private void delegateLayoutContainer(Container parent) {
			if (delegate == null) {
				super.layoutContainer(parent);
			} else {
				delegate.layoutContainer(parent);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void layoutContainer(Container parent) {
			delegateLayoutContainer(parent);
			if (statusBar == null || !statusBar.isVisible())
				return;
			Rectangle b = parent.getBounds();
			Insets i = getInsets();
			int w = b.width - i.right - i.left;
			// int h = b.height - i.top - i.bottom;
			Dimension statusPref = statusBar.getPreferredSize();
			statusBar.setBounds(i.right, b.height - i.bottom - statusPref.height, w, statusPref.height);
			if (contentPane != null) {
				Rectangle bounds = contentPane.getBounds();
				contentPane.setBounds(bounds.x, bounds.y, bounds.width, bounds.height - statusPref.height);
			}

		}
	}

	/**
	 * The current status bar for this root pane.
	 */
	protected JXStatusBar statusBar;

	private JToolBar toolBar;

	/**
	 * The button that gets activated when the pane has the focus and a
	 * UI-specific action like pressing the <b>ESC</b> key occurs.
	 */
	private JButton cancelButton;

	/**
	 * Creates an extended root pane.
	 */
	public JXRootPane() {
		installKeyboardActions();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected Container createContentPane() {
		JComponent c = new JXPanel() {
			/**
			 * {@inheritDoc}
			 */
			@Override
			protected void addImpl(Component comp, Object constraints, int index) {
				synchronized (getTreeLock()) {
					super.addImpl(comp, constraints, index);
					registerStatusBar(comp);
				}
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public void remove(int index) {
				synchronized (getTreeLock()) {
					unregisterStatusBar(getComponent(index));
					super.remove(index);
				}
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public void removeAll() {
				synchronized (getTreeLock()) {
					for (Component c : getComponents()) {
						unregisterStatusBar(c);
					}

					super.removeAll();
				}
			}
		};
		c.setName(this.getName() + ".contentPane");
		c.setLayout(new BorderLayout() {
			/*
			 * This BorderLayout subclass maps a null constraint to CENTER.
			 * Although the reference BorderLayout also does this, some VMs
			 * throw an IllegalArgumentException.
			 */
			@Override
			public void addLayoutComponent(Component comp, Object constraints) {
				if (constraints == null) {
					constraints = BorderLayout.CENTER;
				}
				super.addLayoutComponent(comp, constraints);
			}
		});
		return c;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setLayout(LayoutManager layout) {
		if (layout instanceof XRootLayout) {
			// happens if decoration is uninstalled by ui
			if ((layout != null) && (layout == getLayout())) {
				((XRootLayout) layout).setLayoutManager(null);
			}
			super.setLayout(layout);
		} else {
			if (layout instanceof LayoutManager2) {
				((XRootLayout) getLayout()).setLayoutManager((LayoutManager2) layout);
				if (!isValid()) {
					invalidate();
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected LayoutManager createRootLayout() {
		return new XRootLayout();
	}

	/**
	 * PENDING: move to UI
	 * 
	 */
	private void installKeyboardActions() {
		Action escAction = new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent evt) {
				JButton cancelButton = getCancelButton();
				if (cancelButton != null) {
					cancelButton.doClick(20);
				}
			}

			/**
			 * Overridden to hack around #566-swing: JXRootPane eats escape
			 * keystrokes from datepicker popup. Disable action if there is no
			 * cancel button.
			 * <p>
			 * 
			 * That's basically what RootPaneUI does - only not in the
			 * parameterless isEnabled, but in the one that passes in the sender
			 * (available in UIAction only). We can't test nor compare against
			 * core behaviour, UIAction has sun package scope.
			 * <p>
			 * 
			 * Cont'd (Issue #1358-swingx: popup menus not closed) The extended
			 * hack is inspired by Rob Camick's <a href=
			 * "http://tips4java.wordpress.com/2010/10/17/escape-key-and-dialog/"
			 * > Blog </a> and consists in checking if the the rootpane has a
			 * popup's actionMap "inserted". NOTE: this does not work if the
			 * popup or any of its children is focusOwner.
			 */
			@Override
			public boolean isEnabled() {
				Component component = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
				if (component instanceof JComponent) {
					Action cancelPopup = ((JComponent) component).getActionMap().get("cancel");
					if (cancelPopup != null)
						return false;
				}
				return (cancelButton != null) && (cancelButton.isEnabled());
			}
		};
		getActionMap().put("esc-action", escAction);
		InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
		KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
		im.put(key, "esc-action");
	}

	private void registerStatusBar(Component comp) {
		if (statusBar == null || comp == null) {
			return;
		}
		if (comp instanceof Container) {
			Component[] comps = ((Container) comp).getComponents();
			for (int i = 0; i < comps.length; i++) {
				registerStatusBar(comps[i]);
			}
		}
	}

	private void unregisterStatusBar(Component comp) {
		if (statusBar == null || comp == null) {
			return;
		}
		if (comp instanceof Container) {
			Component[] comps = ((Container) comp).getComponents();
			for (int i = 0; i < comps.length; i++) {
				unregisterStatusBar(comps[i]);
			}
		}
	}

	/**
	 * Set the status bar for this root pane. Any components held by this root
	 * pane will be registered. If this is replacing an existing status bar then
	 * the existing component will be unregistered from the old status bar.
	 * 
	 * @param statusBar
	 *            the status bar to use
	 */
	public void setStatusBar(JXStatusBar statusBar) {
		JXStatusBar oldStatusBar = this.statusBar;
		this.statusBar = statusBar;

		Component[] comps = getContentPane().getComponents();
		for (int i = 0; i < comps.length; i++) {
			// Unregister the old status bar.
			unregisterStatusBar(comps[i]);

			// register the new status bar.
			registerStatusBar(comps[i]);
		}
		if (oldStatusBar != null) {
			remove(oldStatusBar);
		}
		if (statusBar != null) {
			add(statusBar);
		}
		firePropertyChange("statusBar", oldStatusBar, getStatusBar());
	}

	/**
	 * Gets the currently installed status bar.
	 * 
	 * @return the current status bar
	 */
	public JXStatusBar getStatusBar() {
		return statusBar;
	}

	/**
	 * Set the toolbar bar for this root pane. If a tool bar is currently
	 * registered with this {@code JXRootPane}, then it is removed prior to
	 * setting the new tool bar. If an implementation needs to handle more than
	 * one tool bar, a subclass will need to override the singleton logic used
	 * here or manually add toolbars with {@code getContentPane().add}.
	 * 
	 * @param toolBar
	 *            the toolbar to register
	 */
	public void setToolBar(JToolBar toolBar) {
		JToolBar oldToolBar = getToolBar();
		this.toolBar = toolBar;

		if (oldToolBar != null) {
			getContentPane().remove(oldToolBar);
		}

		getContentPane().add(BorderLayout.NORTH, this.toolBar);

		// ensure the new toolbar is correctly sized and displayed
		getContentPane().validate();

		firePropertyChange("toolBar", oldToolBar, getToolBar());
	}

	/**
	 * The currently installed tool bar.
	 * 
	 * @return the current tool bar
	 */
	public JToolBar getToolBar() {
		return toolBar;
	}

	/**
	 * Sets the <code>cancelButton</code> property, which determines the current
	 * default cancel button for this <code>JRootPane</code>. The cancel button
	 * is the button which will be activated when a UI-defined activation event
	 * (typically the <b>ESC</b> key) occurs in the root pane regardless of
	 * whether or not the button has keyboard focus (unless there is another
	 * component within the root pane which consumes the activation event, such
	 * as a <code>JTextPane</code>). For default activation to work, the button
	 * must be an enabled descendant of the root pane when activation occurs. To
	 * remove a cancel button from this root pane, set this property to
	 * <code>null</code>.
	 *
	 * @param cancelButton
	 *            the <code>JButton</code> which is to be the cancel button
	 * @see #getCancelButton()
	 *
	 * @beaninfo description: The button activated by default for cancel actions
	 *           in this root pane
	 */
	public void setCancelButton(JButton cancelButton) {
		JButton old = this.cancelButton;

		if (old != cancelButton) {
			this.cancelButton = cancelButton;

			if (old != null) {
				old.repaint();
			}
			if (cancelButton != null) {
				cancelButton.repaint();
			}
		}

		firePropertyChange("cancelButton", old, cancelButton);
	}

	/**
	 * Returns the value of the <code>cancelButton</code> property.
	 * 
	 * @return the <code>JButton</code> which is currently the default cancel
	 *         button
	 * @see #setCancelButton
	 */
	public JButton getCancelButton() {
		return cancelButton;
	}

}
